import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.Date;
import java.util.concurrent.CopyOnWriteArrayList;

import processing.core.*;


public class IssuePlayer extends PApplet {
	private static final long serialVersionUID = 1L;

	private PApplet pInstance = null;
	private IssuePlayerProject ipp = null;
	private Tooltip ipTooltip = null;

	// Widgets
	private Controls ipControls = null;
	private Priority ipPriority = null;
	private TimeLine ipTimeLine = null;
	private CategoryBins ipBins = null;
	private DeveloperBins ipDevBins = null;
	
	// Fonts
	private PFont fontTitle = null;
	private PFont fontDate = null;
	private PFont fontHelp = null;

	
	private CopyOnWriteArrayList<Issue> colIssues = null;
	private CopyOnWriteArrayList<User> colUsersSubmittedBy = null;
	private CopyOnWriteArrayList<User> colUsersAssignedTo = null;
	
	private boolean projectLoaded = false;

	private int oldWidth = 0;
	private int oldHeight = 0;
	
	private Thread tLoader = null;
	private Thread tUpdater = null;
	
	private String openDate = "";
	
	// IssuePlayer States
	public int ipState = 0;
	public long startTime = 0;
	public int maxTime = 0;
	public double currentTime = 0;
	public int timeInc = 1;
	public double timeMultiple = 0;
	
	private int lastTime = 0;
	
	public boolean showBackground = false;
	public boolean showControls = true;
	public boolean showDevSubmittedBy = true;
	public boolean showDevAssignedTo = true;
	public boolean showBins = true;
	public boolean showDetails = false;
	public boolean showPriority = true;
	public boolean showTimeLine = true;
	public boolean showTooltip = true;
	public boolean showHelp = false;
	
	// TimeLine Dragging
	private int dragX = -1;
	
	// IssuePlayer Constants
	final public int PAUSE = 0;
	final public int FORWARD = 1;
	final public int REVERSE = 2;
	
	// IssuePlayer Settings
	private int currentBackground = 0;
	

	public static void main(String[] args) {
		PApplet.main(new String[] { "IssuePlayer" });
	}

	public void setup() {
		loadSettings();

		this.fontTitle = createFont("FFScala", 20);
		this.fontDate = createFont("FFScala", 14);
		this.fontHelp = createFont("FFScala", 12);

		pInstance = this;
		
		ipp = new IssuePlayerProject();
		ipTooltip = new Tooltip(pInstance, ipp);
		ipControls = new Controls(pInstance, ipTooltip);
		ipPriority = new Priority(pInstance, ipTooltip);
		ipTimeLine = new TimeLine(pInstance, ipTooltip);
		ipBins = new CategoryBins(pInstance, ipTooltip);
		ipDevBins = new DeveloperBins(pInstance, ipTooltip);
		colIssues = new CopyOnWriteArrayList<Issue>();
		colUsersSubmittedBy = new CopyOnWriteArrayList<User>();
		colUsersAssignedTo = new CopyOnWriteArrayList<User>();

		loadProject();
	}

	public void update() {
		if(projectLoaded) {
			if(showDetails)
				ipTooltip.update(width*2/5, height*1/4, width, height, showDetails, showBackground);
			else
				ipTooltip.update(width-470, height-150, width, height, showDetails, showBackground);				
			
			ipTimeLine.update(5, height*3/4, width*2/5, height, showBackground);

			if(showDevSubmittedBy && showDevAssignedTo) {
				ipDevBins.update(0, height*1/20, width*2/10, height*3/4, showBackground, showDevSubmittedBy, showDevAssignedTo);
				ipBins.update(width*2/10, height*1/20, width, height*3/4, 30, showBackground);
			} else if(showDevSubmittedBy || showDevAssignedTo) {
				ipDevBins.update(0, height*1/20, width*1/10, height*3/4, showBackground, showDevSubmittedBy, showDevAssignedTo);
				ipBins.update(width*1/10, height*1/20, width, height*3/4, 30, showBackground);				
			} else {
				ipDevBins.update(0, 0, 0, 0, showBackground, showDevSubmittedBy, showDevAssignedTo);
				ipBins.update(0, height*1/20, width, height*3/4, 30, showBackground);				
			}

			ipPriority.update(width-250, height-22, width-230, height-2);			
		}
	}
	
	public void updateIssues() {
		if(lastTime != (int)currentTime) {
			lastTime = (int)currentTime;
			
			for(Issue iss : colIssues) {
				if(iss.getOpenDate() <= (currentTime+4*timeInc) && (iss.getCloseDate() > currentTime || iss.getCloseDate() == 0))
					ipBins.addIssue(iss);
				else
					ipBins.removeIssue(iss);
												
				if(iss.getOpenDate() <= (currentTime) && (iss.getCloseDate() > currentTime || iss.getCloseDate() == 0)) {
					if(iss.getSubmittedBy() != null)
						iss.getSubmittedBy().addIssue(iss);
					if(iss.getAssignedTo() != null)
						iss.getAssignedTo().addIssue(iss);							
				} else {
					if(iss.getSubmittedBy() != null)
						iss.getSubmittedBy().removeIssue(iss);
					if(iss.getAssignedTo() != null)
						iss.getAssignedTo().removeIssue(iss);												
				}
			}
			
			ipDevBins.updateUsers();
		}	
	}
	
	public void updateTime() {
		if(showTooltip && showDetails)
			return;
		
		currentTime += timeInc*timeMultiple/frameRate;
		
		if(currentTime < 0) {
			currentTime = 0;
			lastTime = -1;
			timeMultiple = 0;
			ipState = PAUSE;
		}
		if(currentTime >= maxTime) {
			currentTime = maxTime;
			lastTime = -1;
			timeMultiple = 0;
			ipState = PAUSE;
		}
	}
	
	@SuppressWarnings("deprecation")
	private String formatDate(Date d) {
		String months[] = {"Jan", "Feb", "Mar", "Apr", "May", "June", "July", "Aug", "Sept", "Oct", "Nov", "Dec"};
		String days[] = {"Mon", "Tues", "Wed", "Thurs", "Fri", "Sat", "Sun"};
		return days[d.getDay()] + ", " + months[d.getMonth()] + " " + d.getDate() + ", " + (d.getYear()+1900);
	}
	
	public void draw() {
		background(currentBackground);
		
		ipControls.display();

		textFont(fontTitle);
		if(!projectLoaded) {
			textAlign(CENTER, CENTER);
			text("Loading", width/2, height/2);
			return;
		}
		textAlign(CENTER, TOP);
		text(ipp.getProjectName(), width/2, 0);
		textFont(fontDate);
		text("Start Date: "+openDate, width/2, fontTitle.theight);

		if(oldWidth != width || oldHeight != height) {
			oldWidth = width;
			oldHeight = height;
			update();
		}
		
		updateTime();

		if(showDevSubmittedBy || showDevAssignedTo)
			ipDevBins.display();

		if(showBins)
			ipBins.display(currentTime, timeInc);

		if(showPriority)
			ipPriority.display();
		
		if(showTimeLine)
			ipTimeLine.display(currentTime, timeInc);
		
		if(showTooltip)
			if(dragX == -1)
				ipTooltip.display();
			else
				ipTooltip.displayLockedOnly();
		
		if(showHelp)
			displayHelp();
	}
	
	public void mouseClicked() {
		if(showControls)
			switch(ipControls.isClicked()) {
			case Controls.STOP:
				stateStop();
				return;
			case Controls.REWIND:
				stateRewind();
				return;
			case Controls.PLAYPAUSE:
				statePlayPause();
				return;
			case Controls.FASTFORWARD:
				stateFastForward();
				return;
			case Controls.EJECT:
				stateEject();
				return;
			}

		if(showTooltip && showDetails && ipTooltip.isLocked() && mouseButton == RIGHT) {
			showDetails = false;
			update();
			return;
		}		

		if(showBins && ipBins.isClicked()) {
			for(Issue i : colIssues)
				if(i.isClicked(currentTime, timeInc)) {
					ipTooltip.setLockedValue(i);
					if (mouseButton == RIGHT) {
						showDetails = !showDetails;
						update();
					}
					return;
				}
			return;
		}
		
		if(showDevSubmittedBy || showDevAssignedTo) {
			ipDevBins.isClicked();
		}

		if(showPriority)
			ipPriority.isClicked();
	}

	public void mouseDragged() {
		if(dragX == -1) {
			if(showTimeLine && ipTimeLine.isOver())
				dragX = mouseX;
		} else if (dragX != mouseX) {
			currentTime += -(dragX-mouseX)*timeInc; // BARXSPACING = 4, seemed too slow
			dragX = mouseX;
		}
	}
	
	public void mousePressed() {
		if(showTimeLine && ipTimeLine.isOver()) {
			timeMultiple = 0;
			ipState = PAUSE;
		}		
	}
	
	public void mouseReleased() {
		dragX = -1;
	}
	
	public void keyPressed() {
		switch(key) {
		case ESC:
			timeMultiple = 0;
			ipState = PAUSE;
			key = 0;
			break;
		case ' ':
			statePlayPause();
			break;
		case '-':
			stateRewind();
			break;
		case '=':
		case '+':
			stateFastForward();
			break;
		case ENTER:
		case RETURN:
			stateStop();
			statePlayPause();
			break;
		case 'b':
			showBackground = !showBackground;
			update();
			break;
		case 'd':
			if(showDevSubmittedBy && showDevAssignedTo) {
				showDevSubmittedBy = false;
				showDevAssignedTo = false;
			} else if(showDevSubmittedBy) {
				showDevSubmittedBy = false;
				showDevAssignedTo = true;
			} else if(showDevAssignedTo) {
				showDevSubmittedBy = true;
			} else {
				showDevSubmittedBy = true;
			}
			update();
			break;
		case 'p':
			showPriority = !showPriority;
			update();
			break;
		case 't':
			showTimeLine = !showTimeLine;
			update();
			break;
		case '`':
			showControls = !showControls;
			update();
			break;
		case 'u':
			showTooltip = !showTooltip;
			update();
			break;
		case 'x':
			showBins = !showBins;
			update();
			break;
		case 'h':
			showHelp = !showHelp;
			break;
		case 's':
			if(timeInc == 1)
				timeInc = 7;
			else if(timeInc == 7)
				timeInc = 30;
			else
				timeInc = 1;
			break;
		}
	}

	private void loadSettings() {
		size(800,600);
		frame.setTitle("IssuePlayer");
		frame.setResizable(true);
		frame.addComponentListener(new ComponentAdapter() {  
			public void componentResized(ComponentEvent e) {  
				if(e.getSource()==frame) {
					if(frame.getHeight() < 600)
						if(frame.getWidth() < 800)
							frame.setSize(800, 600);
						else
							frame.setSize(frame.getWidth(), 600);
					else
						if(frame.getWidth() < 800)
							frame.setSize(800, frame.getHeight());
				}  
			}  
		});
	}
	  
	private void loadProject() {		
		tUpdater = new Thread(new Runnable() {
			public void run() {
				while(projectLoaded)
					updateIssues();
			}
		});
		
		tLoader = new Thread(new Runnable() {
			public void run() {
				ProjectLoader pl = new ProjectLoader(ipp);
				if(pl.isCancelled() || !ipp.isProjectOpen())
					return;

				projectLoaded = false;
				
				colIssues.clear();
				colUsersSubmittedBy.clear();
				colUsersAssignedTo.clear();
				ipp.closeProject();

				ipState = PAUSE;
				startTime = 0;
				maxTime = 0;
				currentTime = 0;
				lastTime = -1;
				timeMultiple = 0;

				if(!ipp.loadUsers(colUsersSubmittedBy, colUsersAssignedTo, pInstance)) {
					cannotLoadUsers();
					return;
				}
				if(!ipp.loadIssues(colIssues, colUsersSubmittedBy, colUsersAssignedTo, pInstance, ipTooltip, ipPriority)) {
					cannotLoadIssues();
					return;
				}
				if(!ipp.loadCategories(ipBins)) {
					cannotLoadCategories();
					return;
				}
				
				startTime = ipp.getStartTime();
				ipTooltip.setStartTime(startTime);
				openDate = formatDate(new Date(startTime));
				maxTime = ipp.getMaxTime();
				currentTime = 0;
				lastTime = -1;
				timeMultiple = 0;
				ipTimeLine.loadIssues(colIssues, startTime, maxTime);
				ipBins.setMaxIssuesPerBin(colIssues.size()/10); // TODO needs update
				ipDevBins.loadUsers(colUsersSubmittedBy, colUsersAssignedTo);

				frame.setTitle("IssuePlayer - " + ipp.getProjectName());
				projectLoaded = true;
				update();
				tUpdater.start();
			}
		});
		tLoader.start();
		ipState = PAUSE;
	}

	private void ejectProject() {		
		loadProject();
	}

	private void cannotLoadUsers() {
		System.err.println("Cannot load users!");
	}

	private void cannotLoadIssues() {
		System.err.println("Cannot load issues!");
	}
	
	private void cannotLoadCategories() {
		System.err.println("Cannot load categories!");
	}

	private void stateStop() {
		currentTime = 0;
		timeMultiple = 0;
		ipState = PAUSE;
	}

	private void stateRewind() {
		switch(ipState) {
		case REVERSE:
			timeMultiple *= 2;
			break;
		case FORWARD:
			timeMultiple /= 2;
			if(timeMultiple >= 0.5)
				break;
		default:
			timeMultiple = -1;
			ipState = REVERSE;
			break;
		}
	}

	private void statePlayPause() {
		if(ipState == PAUSE) {
			timeMultiple = 1;
			ipState = FORWARD;
			if(currentTime >= maxTime) {
				currentTime = 0;
				lastTime = -1;
			}
		} else {
			timeMultiple = 0;
			ipState = PAUSE;
		}
	}
	
	private void stateFastForward() {
		switch(ipState) {
		case FORWARD:
			timeMultiple *= 2;
			break;
		case REVERSE:
			timeMultiple /= 2;
			if(timeMultiple <= -0.5)
				break;
		default:
			timeMultiple = 1;
			ipState = FORWARD;
			break;
		}
	}

	private void stateEject() {
		ejectProject();
	}

	private void displayHelp() {
		final String help[][] = {
			{"{SPACE}","Play/Pause"},
			{"'-'","Rewind"},
			{"'+'","Fast Forward"},
			{"{ENTER}","Restart Playback"},
			{"{RETURN}","Restart Playback"},
			{"'D'","Toggle Developer Display Modes"},
			{"'P'","Toggle Priority Bar"},
			{"'T'","Toggle TimeLine"},
			{"'`'","Toggle Controls"},
			{"'U'","Toogle Tooltips"},
			{"'X'","Toggle Issue Bins"},
			{"'H'","Toggle Help"},
			{"'S'","Switch Between Daily, Weekly, Monthly Display"},
			{"Left Click","Locks the detailed display to issue or user"},
			{"Right Click","On issues or detail frame, expands/contracts details"},
			{"'B'","Show Background (Debugging)"}
		};
		final int x = 250;
		final int y = (help.length+1)*fontHelp.theight;

		timeMultiple = 0;
		ipState = PAUSE;
		
		int bgx1 = width/2 - x;
		int bgy1 = height/2 - y/2;
		int bgx2 = width/2 + x;
		int bgy2 = height/2 + y/2;
		
		stroke(color(245, 245, 220)); // beige
		fill(color(245, 245, 220)); // beige
		rectMode(CORNERS);
		rect(bgx1, bgy1, bgx2, bgy2);

		stroke(color(0, 0, 0)); // black
		fill(color(0, 0, 0)); // black
		textFont(fontHelp);

		textAlign(CENTER, TOP);
		text("Help:", width/2, bgy1);
		textAlign(LEFT, TOP);
		for(int i = 0; i < help.length; i++) {
			text(help[i][0], bgx1+fontHelp.twidth, bgy1+((i+1)*fontHelp.theight));
			text(help[i][1], bgx1+(fontHelp.twidth*8), bgy1+((i+1)*fontHelp.theight));
		}
	}	
}
